package jmine.tec.script.persistent.context.impl;

import java.util.Collection;
import java.util.Map;

import jmine.tec.script.ScriptExecutionContext;
import jmine.tec.script.ScriptExecutor;
import jmine.tec.script.impl.ScriptParametersImpl;
import jmine.tec.script.persistent.PersistentScriptRuntimeException;
import jmine.tec.script.persistent.ScriptController;
import jmine.tec.script.persistent.ScriptPersistenceMessages;
import jmine.tec.script.persistent.context.PersistentScriptExecutionContext;
import jmine.tec.script.persistent.context.ScriptExecutionContextFactory;
import jmine.tec.script.persistent.context.ScriptExecutorRule;
import jmine.tec.script.persistent.domain.Script;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.collection.impl.CopyOnWriteHashMap;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Factory de contextos de execução de scripts persistíveis.
 * 
 * @author lundberg
 */
public class ScriptExecutionContextFactoryImpl implements ScriptExecutionContextFactory {

    private ScriptController controller;

    private Map<Tuple<String, String>, Class<? extends ScriptExecutor>> executorMap =
            new CopyOnWriteHashMap<Tuple<String, String>, Class<? extends ScriptExecutor>>();

    private Map<String, Class<?>> classMap = new CopyOnWriteHashMap<String, Class<?>>();

    /**
     * {@inheritDoc}
     */
    public ScriptExecutionContext<?> createContext(Script script) {
        return this.doCreateContext(ScriptExecutionContext.class, this.getExecutionContextClass(script), script);
    }

    /**
     * {@inheritDoc}
     */
    public <C> C createContext(Class<C> contextInterface, Script script) {
        return this.doCreateContext(contextInterface, this.getExecutionContextClass(script), script);
    }

    /**
     * {@inheritDoc}
     */
    public ScriptExecutionContext<?> createDependencyContext(Script script) {
        return this.doCreateContext(ScriptExecutionContext.class, this.getDependencyContextClass(script), script);
    }

    /**
     * {@inheritDoc}
     */
    public <C> C createDependencyContext(Class<C> contextInterface, Script script) {
        return this.doCreateContext(contextInterface, this.getDependencyContextClass(script), script);
    }

    /**
     * {@inheritDoc}
     */
    public <C> C createValidationContext(Class<C> contextInterface, Script script) {
        return this.doCreateContext(contextInterface, this.getValidationContextClass(script), script);
    }

    /**
     * {@inheritDoc}
     */
    public ScriptExecutionContext<?> createValidationContext(Script script) {
        return this.doCreateContext(ScriptExecutionContext.class, this.getValidationContextClass(script), script);
    }

    /**
     * Cria o contexto do script, que deve ser subclasse de contextInterface e uma instância de contextClass, com a configuração apropriada
     * para o script.
     * 
     * @param contextInterface contextInterface
     * @param contextClass contextClass
     * @param script script
     * @param <C> interface
     * @param <E> classe concreta efetiva do contexto
     * @return context
     */
    private <C, E extends PersistentScriptExecutionContext> C doCreateContext(Class<C> contextInterface, Class<E> contextClass,
            Script script) {
        PersistentScriptExecutionContext<?> context = this.instantiateContext(contextInterface, contextClass);
        context.setParameters(new ScriptParametersImpl());
        context.setExecutorClass(this.getExecutorClass(script));
        context.setScript(script);
        context.setScriptController(this.controller);
        context.setDispatcherKey(script.getPurpose().getMnemonico());
        return contextInterface.cast(context);
    }

    /**
     * Instancia o contexto
     * 
     * @param <C> tipo de contexto que será criado
     * @param <E> classe concreta que será instanciada
     * @param contextInterface interface ou superclasse que o bean resultante deve implementar
     * @param contextClass classe concreta do contexto que será instanciado
     * @return PersistentScriptExecutionContext
     */
    private <C, E extends PersistentScriptExecutionContext> PersistentScriptExecutionContext<?> instantiateContext(
            Class<C> contextInterface, Class<E> contextClass) {
        if (!contextClass.isAssignableFrom(contextClass)) {
            throw new PersistentScriptRuntimeException(ScriptPersistenceMessages.SCRIPT_CONTEXT_DOES_NOT_MATCH.create(
                    contextClass.getName(), contextClass.getName()));
        }
        try {
            return ReflectionUtils.newInstance(contextClass);
        } catch (IllegalArgumentException e) {
            throw new PersistentScriptRuntimeException(ScriptPersistenceMessages.ERROR_CREATING_CONTEXT.create(), e);
        } catch (SecurityException e) {
            throw new PersistentScriptRuntimeException(ScriptPersistenceMessages.ERROR_CREATING_CONTEXT.create(), e);
        }
    }

    /**
     * Encontra a classe (ou interface) do contexto de execução do script.
     * 
     * @param script script
     * @return class
     */
    private Class<? extends PersistentScriptExecutionContext> getExecutionContextClass(Script script) {
        return this.findClass(PersistentScriptExecutionContext.class, script.getPurpose().getExecutionContext());
    }

    /**
     * Encontra a classe (ou interface) do contexto de obtenção de dependências do script.
     * 
     * @param script script
     * @return class
     */
    private Class<? extends PersistentScriptExecutionContext> getDependencyContextClass(Script script) {
        return this.findClass(PersistentScriptExecutionContext.class, script.getPurpose().getDependencyContext());
    }

    /**
     * Encontra a classe (ou interface) do contexto de validação do script.
     * 
     * @param script script
     * @return class
     */
    private Class<? extends PersistentScriptExecutionContext> getValidationContextClass(Script script) {
        return this.findClass(PersistentScriptExecutionContext.class, script.getPurpose().getValidationContext());
    }

    /**
     * Encontra a classe (ou interface) do contexto de execução do script.
     * 
     * @param script script
     * @return class
     */
    private Class<? extends ScriptExecutor> getExecutorClass(Script script) {
        Tuple<String, String> ruleKey =
                new UnmodifiableTuple<String, String>(script.getPurpose().getMnemonico(), script.getLanguage().getMnemonico());
        if (this.executorMap.containsKey(ruleKey)) {
            return this.executorMap.get(ruleKey);
        } else {
            Class<? extends ScriptExecutor> executor = this.findClass(ScriptExecutor.class, script.getLanguage().getExecutorClass());
            this.executorMap.put(ruleKey, executor);
            return executor;
        }
    }

    /**
     * Encontra a classe com o nome dado, verificando que ela é uma subclasse da classe esperada.
     * 
     * @param <K> tipo da classe esperada
     * @param expected classe esperada
     * @param className nome da classe a ser carregada
     * @return classe
     */
    private <K> Class<? extends K> findClass(Class<K> expected, String className) {
        Class<?> baseClass;
        if (this.classMap.containsKey(className)) {
            baseClass = this.classMap.get(className);
        } else {
            try {
                baseClass = ReflectionUtils.findClass(className);
                this.classMap.put(className, baseClass);
            } catch (ClassNotFoundException e) {
                throw new PersistentScriptRuntimeException(ScriptPersistenceMessages.CLASS_NOT_FOUND.create(className), e);
            }
        }
        return baseClass.asSubclass(expected);
    }

    /**
     * {@inheritDoc}
     */
    public void register(Collection<? extends ScriptExecutorRule> registers) {
        for (ScriptExecutorRule rule : registers) {
            this.addExecutorRule(rule.getFinalidade(), rule.getLinguagem(), rule.getExecutor());
        }
    }

    /**
     * Adiciona ao mapa de executores a regra que diz que um script com a finalidade e linguagem dadas deve ser executado por um
     * ScriptExecutor da classe dada.
     * 
     * @param finalidade finalidade
     * @param linguagem linguagem
     * @param executor executor
     */
    private void addExecutorRule(String finalidade, String linguagem, Class<? extends ScriptExecutor> executor) {
        this.executorMap.put(new UnmodifiableTuple<String, String>(finalidade, linguagem), executor);
    }

    /**
     * @param controller the controller to set
     */
    public void setController(ScriptController controller) {
        this.controller = controller;
    }
}
